{
  Copyright 2012 Sergey Ostanin

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
}

unit PadEditor;

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, Shape,
  PadWidget, superobject, LCLType, VisualUtils, base64, Pad, Scene, MiscUtils;

type
  TPadEditorFrame = class(TShapeFrame)
  private
    FWidget: TPadWidget;
    FRendered: Boolean;
    FCachedImages: TStringList { TShapeId -> TCachedImage };
    function GetPadColor: TColor;
    procedure SetPadColor(Value: TColor);
    function GetBorder: Boolean;
    procedure SetBorder(Value: Boolean);
    procedure WidgetInsert(Sender: TPadWidget; Position: Integer; Fragment: TPad);
    procedure WidgetDelete(Sender: TPadWidget; Position, Len: Integer);
    procedure SetUpImage(const ImageId: TShapeId; g: TGraphicPadObject);
    { private declarations }
  public
    procedure AfterConstruction; override;
    destructor Destroy; override;
    procedure Render;

    property Border: Boolean read GetBorder write SetBorder;
    property PadColor: TColor read GetPadColor write SetPadColor;
    { public declarations }
  end;
  TPadEditorFrameList = TGenericObjectList<TPadEditorFrame>;

implementation

{$R *.lfm}

type
  TCachedImage = class
  private
    FImageFormat: String;
    FImageData: String;
    FSourceFormat: String;
    FSourceData: String;
    FBitmap: TBitmap;
    FUsed: Boolean;
  public
    destructor Destroy; override;
  end;

{ TCachedImage }

destructor TCachedImage.Destroy;
begin
  FreeAndNil(FBitmap);
  inherited;
end;

{ TPadEditorFrame }

function TPadEditorFrame.GetPadColor: TColor;
begin
  Result := FWidget.Color;
end;

procedure TPadEditorFrame.SetPadColor(Value: TColor);
begin
  FWidget.Color := Value;
end;

function TPadEditorFrame.GetBorder: Boolean;
begin
  Result := FWidget.Border;
end;

procedure TPadEditorFrame.SetBorder(Value: Boolean);
begin
  FWidget.Border := Value;
end;

procedure TPadEditorFrame.WidgetInsert(Sender: TPadWidget; Position: Integer;
  Fragment: TPad);
var
  i: Integer;
  Obj: TPadObject;
  Input, Blocks, b: ISuperObject;
  g: TGraphicPadObject;
begin
  FWidget.Update;

  Input := SO;
  Input.I['position'] := Position;
  Blocks := SA([]);

  for i := 0 to Fragment.ObjectCount-1 do
  begin
    Obj := Fragment.Objects[i];
    b := SO;

    if Obj is TTextPadObject then
    begin
      b.S['type'] := 'TEXT';
      b.S['text'] := UTF8Encode(TTextPadObject(Obj).Text);
    end
    else if Obj is TGraphicPadObject then
    begin
      b.S['type'] := 'IMAGE';
      g := TGraphicPadObject(Obj);
      b.S['imageFormat'] := g.ImageFormat;
      b.S['imageData'] := EncodeStringBase64(g.ImageData);
      if g.SourceFormat <> '' then
      begin
        b.S['sourceFormat'] := g.SourceFormat;
        b.S['sourceData'] := EncodeStringBase64(g.SourceData);
      end;
    end
    else if Obj is TLineFeedPadObject then
      b.S['type'] := 'LINE_FEED'
    else
      Assert( FALSE );

    Blocks.AsArray.Add(b);
  end;

  Input['blocks'] := Blocks;
  Dispatcher.PerformAction(Id, 'insertFragment', Input);
end;

procedure TPadEditorFrame.WidgetDelete(Sender: TPadWidget; Position, Len: Integer);
var
  Input: ISuperObject;
begin
  FWidget.Update;

  Input := SO;
  Input.I['position'] := Position;
  Input.I['length'] := Len;
  Dispatcher.PerformAction(Id, 'deleteFragment', Input);
end;

procedure TPadEditorFrame.SetUpImage(const ImageId: TShapeId; g: TGraphicPadObject);
var
  c: TCachedImage;
  Index: Integer;
  Image: ISuperObject;
  Graphic: TGraphic;
  Bitmap: TBitmap;
begin
  Index := FCachedImages.IndexOf(ImageId);
  if Index = -1 then
  begin
    c := TCachedImage.Create;
    try
      Image := Scene.Shapes[ImageId];
      c.FImageFormat := Image.S['format'];
      c.FImageData := DecodeStringBase64(Image.S['data']);
      if Image['source'] <> nil then
      begin
        c.FSourceFormat := Image.S['source.format'];
        c.FSourceData := DecodeStringBase64(Image.S['source.data']);
      end;

      Graphic := CreateGraphic(c.FImageFormat, c.FImageData);
      try
        c.FBitmap := TBitmap.Create;
        c.FBitmap.Assign(Graphic);
      finally
        Graphic.Free;
      end;

      FCachedImages.AddObject(ImageId, c);
    except
      c.Free;
      raise;
    end;
  end
  else
    c := TCachedImage(FCachedImages.Objects[Index]);

  c.FUsed := TRUE;

  g.ImageFormat := c.FImageFormat;
  g.ImageData := c.FImageData;
  g.SourceFormat := c.FSourceFormat;
  g.SourceData := c.FSourceData;

  Bitmap := TBitmap.Create;
  try
    Bitmap.Assign(c.FBitmap);
    g.CachedImage := Bitmap;
    Bitmap := nil;
  finally
    Bitmap.Free;
  end;
end;

procedure TPadEditorFrame.AfterConstruction;
begin
  inherited;
  FWidget := TPadWidget.Create(nil);
  FWidget.Name := 'editor';
  FWidget.Align := alClient;
  FWidget.OnInsert := WidgetInsert;
  FWidget.OnDelete := WidgetDelete;
  FWidget.Parent := Self;
  FWidget.TabStop := TRUE;

  FCachedImages := TStringList.Create;
  FCachedImages.OwnsObjects := TRUE;
end;

destructor TPadEditorFrame.Destroy;
begin
  FreeAndNil(FWidget);
  FreeAndNil(FCachedImages);
  inherited;
end;

procedure TPadEditorFrame.Render;
var
  Block: ISuperObject;
  BlockType: String;
  g: TGraphicPadObject;
  i: Integer;
  ShapeChanged: Boolean;
begin
  ShapeChanged := UpdateShape;
  if not FRendered or ShapeChanged then
  begin
    for i := 0 to FCachedImages.Count-1 do
      TCachedImage(FCachedImages.Objects[i]).FUsed := FALSE;

    FWidget.Model.BeginModify;
    try
      FWidget.Model.Clear;

      for Block in Shape['block'] do
      begin
        BlockType := Block.S['type'];
        if BlockType = 'TEXT' then
          FWidget.Model.AddText(UTF8Decode(Block.S['textBlock.text']))
        else if BlockType = 'IMAGE' then
        begin
          g := TGraphicPadObject.Create;
          FWidget.Model.AddObject(g);
          SetUpImage(Block.S['imageBlock.image'], g);
        end
        else if BlockType = 'LINE_FEED' then
          FWidget.Model.AddObject(TLineFeedPadObject.Create)
        else
          Assert( FALSE );
      end;
    finally
      FWidget.Model.EndModify;
    end;

    for i := FCachedImages.Count-1 downto 0 do
    begin
      if not TCachedImage(FCachedImages.Objects[i]).FUsed then
        FCachedImages.Delete(i);
    end;
  end;
  FRendered := TRUE;
end;

end.

